package algo;

import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import junit.framework.Assert;
import solverStrategies.NakedSingle;
import sudoku.SudokuEinzelfeld;
import sudoku.SudokuSpielfeld;

public class SolveSudoku {

	private int counter = 0;
	private boolean changes = true;
	
	public static void main(String[] args) {
		SudokuSpielfeld spielfeld = new SudokuSpielfeld();
		new SolveSudoku().solveSudoku(spielfeld);
	}

	public void solveSudoku(SudokuSpielfeld spielfeld) {
		long start = System.currentTimeMillis();
		spielfeld = new SudokuSpielfeld("src/main/resources/sudoku.txt");

		Assert.assertTrue(spielfeld.isValid(spielfeld.getSudokuFeld()));
		Assert.assertFalse(spielfeld.isReady(spielfeld.getSudokuFeld()));
		spielfeld.printSudoku();
		while (!spielfeld.isReady(spielfeld.getSudokuFeld())) {
			changes = false;

			NakedSingle nakedsingle = new NakedSingle();
			changes = nakedsingle.manipulateSudoku(spielfeld);
			System.out.println("\nChanges? " + changes);

			spielfeld.printSudoku();
			if (!changes) {
				completeBlockReduceAlgo(spielfeld);

				if (changes) {
					System.out.println("Block reduction set a number");
				}
			}

			if (!changes) {
				completeRowReduceAlgo(spielfeld);

				if (changes) {
					System.out.println("Row reduction set a number");

				}
			}

			if (!changes) {
				completeColReduceAlgo(spielfeld);

				if (changes) {
					System.out.println("Col reduction set a number");

				}
			}
			// LEVEL 3
			if (!changes) {
				check(spielfeld);
			}
			System.out.println("CHECKPO");
			// break;
		}

		System.out.printf("Needed %d Steps.", counter);
		long end = System.currentTimeMillis();
		System.out.printf("Takes %d Milliseconds", end - start);
	}

	/*
	 * ######### LEVEL 2 Algo #################
	 */

	private void completeColReduceAlgo(SudokuSpielfeld spielfeld) {
		System.out
				.println("No clear choice possble. Try next algorithm (colSet reduce)");
		for (int i = 0; i < 9; i++) {
			System.out.println("Colnr: " + i);
			Map<Integer, Set<Integer>> compressedsBlocks = reduceWithLookUpForColSets(
					spielfeld, i);
			Set<Integer> ids = compressedsBlocks.keySet();
			List<Integer> sortedIds = new LinkedList<Integer>(ids);
			Collections.sort(sortedIds);
			for (Integer id : sortedIds) {
				Set<Integer> set = compressedsBlocks.get(id);
				System.out.print("ID: " + id + ":" + set.toString());
				if (set.size() == 1) {
					System.out.println(" ID: " + id + ":" + set.toString());
					spielfeld.setNumberWithID(id, (Integer) set.toArray()[0], "SolveSudoku");
					changes = true;
					return;
				}
			}
			System.out.println();

		}
	}

	private boolean completeBlockReduceAlgo(SudokuSpielfeld spielfeld) {
		System.out
				.println("No clear choice possble. Try next algorithm (blockSet reduce)");
		for (int i = 1; i <= 9; i++) {
			System.out.println("blocknr: " + i);
			Map<Integer, Set<Integer>> compressedsBlocks = reduceWithLookUpForBlockSets(
					spielfeld, i);
			Set<Integer> ids = compressedsBlocks.keySet();
			for (Integer id : ids) {
				Set<Integer> set = compressedsBlocks.get(id);
				System.out.print(" ID: " + id + ":" + set.toString());
				if (set.size() == 1) {
					System.out.println(" ID: " + id + ":" + set.toString());
					spielfeld.setNumberWithID(id, (Integer) set.toArray()[0], "SolveSudoku");
					changes = true;
					return changes;
				}
			}

			System.out.println();
		}
		return false;
	}

	private void completeRowReduceAlgo(SudokuSpielfeld spielfeld) {
		System.out
				.println("No clear choice possble. Try next algorithm (rowSet reduce)");
		for (int i = 0; i < 9; i++) {
			System.out.println("Rownr: " + i);
			Map<Integer, Set<Integer>> compressedsBlocks = reduceWithLookUpForRowSets(
					spielfeld, i);
			Set<Integer> ids = compressedsBlocks.keySet();
			List<Integer> sortedIds = new LinkedList<Integer>(ids);
			Collections.sort(sortedIds);
			for (Integer id : sortedIds) {
				Set<Integer> set = compressedsBlocks.get(id);
				System.out.print("ID: " + id + ":" + set.toString());
				if (set.size() == 1) {
					System.out.println(" ID: " + id + ":" + set.toString());
					spielfeld.setNumberWithID(id, (Integer) set.toArray()[0], "SolveSudoku");
					changes = true;
					return;
				}
			}
			System.out.println();

		}
	}

	private Map<Integer, Set<Integer>> reduceWithLookUpForBlockSets(
			SudokuSpielfeld sf, int blocknr) {
		List<SudokuEinzelfeld> blockFedler = sf.returnblock(blocknr);
		Map<Integer, Set<Integer>> idToEin = new HashMap<Integer, Set<Integer>>();
		for (SudokuEinzelfeld sudokuEinzelfeld : blockFedler) {
			if (sudokuEinzelfeld.getNumber() == 0) {
				Set<Integer> poss = sudokuEinzelfeld.getPossibleNumbers();
				idToEin.put(sudokuEinzelfeld.getId(), poss);
			}
		}
		idToEin = reduceSets(idToEin);
		return idToEin;
	}

	private Map<Integer, Set<Integer>> reduceWithLookUpForRowSets(
			SudokuSpielfeld sf, int rownr) {
		List<SudokuEinzelfeld> blockFedler = sf.returnRow(rownr);
		Map<Integer, Set<Integer>> idToEin = new HashMap<Integer, Set<Integer>>();
		for (SudokuEinzelfeld sudokuEinzelfeld : blockFedler) {
			if (sudokuEinzelfeld.getNumber() == 0) {
				Set<Integer> poss = sudokuEinzelfeld.getPossibleNumbers();
				idToEin.put(sudokuEinzelfeld.getId(), poss);
			}
		}
		idToEin = reduceSets(idToEin);
		return idToEin;
	}

	private Map<Integer, Set<Integer>> reduceWithLookUpForColSets(
			SudokuSpielfeld sf, int colnr) {
		List<SudokuEinzelfeld> blockFedler = sf.returnCol(colnr);
		Map<Integer, Set<Integer>> idToEin = new HashMap<Integer, Set<Integer>>();
		for (SudokuEinzelfeld sudokuEinzelfeld : blockFedler) {
			if (sudokuEinzelfeld.getNumber() == 0) {
				Set<Integer> poss = sudokuEinzelfeld.getPossibleNumbers();
				idToEin.put(sudokuEinzelfeld.getId(), poss);
			}
		}
		idToEin = reduceSets(idToEin);
		return idToEin;
	}

	/*
	 * This method reduce the possible numbers by looking for each number at
	 * every set and counts the number of possible sets
	 */
	private static Map<Integer, Set<Integer>> reduceSets(
			Map<Integer, Set<Integer>> idToEin) {
		Map<Integer, Integer> numbers = new HashMap<Integer, Integer>();

		for (int i = 1; i <= 9; i++) {
			Set<Integer> keys = idToEin.keySet();
			for (Integer integer : keys) {
				Set<Integer> set = idToEin.get(integer);
				if (set.contains(i)) {
					int counter;
					if (numbers.containsKey(i)) {
						counter = numbers.get(i);
						counter++;
					} else {
						counter = 1;
					}
					numbers.put(i, counter);
				}
			}
		}

		Set<Integer> keys = numbers.keySet();
		for (Integer integer : keys) {
			if (numbers.get(integer) == 1) {
				Set<Integer> tmp = new HashSet<Integer>();
				tmp.add(integer);
				Set<Integer> keystmp = idToEin.keySet();
				for (Integer integerTmp : keystmp) {
					Set<Integer> set = idToEin.get(integerTmp);
					if (set.contains(integer)) {
						set.retainAll(tmp);
					}
				}
			}

		}
		return idToEin;
	}

	/*
	 * ######### LEVEL 3 Algo #################
	 */

	private void check(SudokuSpielfeld sf) {
		int successCounter = 0;
		// nummer : row : col
		int[][] arr = new int[10][3];
		for (int block = 1; block < 10; block++) {
			List<SudokuEinzelfeld> blockFedler = sf.returnblock(block);
			// Actual number = 1,2,....9
			for (int actualNumber = 1; actualNumber < 10; actualNumber++) {
				Set<Integer> rows = new HashSet<Integer>();
				Set<Integer> cols = new HashSet<Integer>();
				for (SudokuEinzelfeld sudokuEinzelfeld : blockFedler) {
					if (sudokuEinzelfeld.getNumber() == 0) {
						Set<Integer> poss = sudokuEinzelfeld
								.getPossibleNumbers();
						for (Integer integer : poss) {
							if (integer == actualNumber) {
								cols.add(sudokuEinzelfeld.getCol());
								rows.add(sudokuEinzelfeld.getRow());
							}
						}
					}
				}
				if (rows.size() == 1) {
					List<SudokuEinzelfeld> row = sf.returnRow((Integer) rows
							.toArray()[0]);

					for (SudokuEinzelfeld sudokuEinzelfeld : row) {
						if (sudokuEinzelfeld.getBlockNr() != block
								&& sudokuEinzelfeld.getNumber() == 0) {
							if (sudokuEinzelfeld
									.removeNumberFromPossibleNumbers(actualNumber)) {
								System.out.println("Deleting " + actualNumber
										+ " from row "
										+ sudokuEinzelfeld.getRow()
										+ "if field is not in block " + block);
								System.out.println("ID: "
										+ sudokuEinzelfeld.getId());
								successCounter++;
							}
						}
					}
				}
				if (cols.size() == 1) {
					List<SudokuEinzelfeld> col = sf.returnCol((Integer) cols
							.toArray()[0]);
					for (SudokuEinzelfeld sudokuEinzelfeld : col) {
						if (sudokuEinzelfeld.getBlockNr() != block
								&& sudokuEinzelfeld.getNumber() == 0) {
							if (sudokuEinzelfeld
									.removeNumberFromPossibleNumbers(actualNumber)) {
								System.out.println("Deleting " + actualNumber
										+ " from col "
										+ sudokuEinzelfeld.getCol()
										+ "if field is not in block " + block);
								System.out.println("ID: "
										+ sudokuEinzelfeld.getId());
								successCounter++;
							}
						}
					}
				}
			}
		}
	}
}
